(() => {
  // Communication channel constants
  const CHANNEL_FROM_PAGE = '__apiProxyFromPage';
  const CHANNEL_FROM_EXT = '__apiProxyFromExt';

  function postToContent(cmd, payload) {
    window.postMessage({ __chan: CHANNEL_FROM_PAGE, cmd, payload }, '*');
  }

  // State managed inside page
  let enabled = true;
  let rules = [];

  // 打印信息
  function logInfo() {
    if (typeof console === 'undefined') return;
    console.log("%c[*APIProxy:*] ", "color: blue;background-color: red;", ...arguments );
  }

  // Compile regex once for performance
  function normalizeRules(raw) {
    const list = Array.isArray(raw) ? raw : [];
    return list.map((r) => {
      const nr = { ...r };
      nr.enabled = nr.enabled !== false;
      nr.match = nr.match || {};
      // match: { type: 'string'|'regex', value: '...', methods?: string[] }
      const type = nr.match.type === 'regex' ? 'regex' : 'string';
      const val = String(nr.match.value || '');
      nr._patternType = type;
      nr._patternValue = val;
      nr._methods = Array.isArray(nr.match.methods)
        ? nr.match.methods.map((m) => String(m || '').toUpperCase()).filter(Boolean)
        : [];
      if (type === 'regex') {
        try {
          // allow user to write /pattern/flags or just pattern
          const m = val.match(/^\/(.*)\/([gimsuy]*)$/);
          if (m) {
            nr._regex = new RegExp(m[1], m[2]);
          } else {
            nr._regex = new RegExp(val);
          }
        } catch (e) {
          nr._regex = null;
        }
      }
      // action: { type: 'modifyRequest'|'mockResponse'|'modifyResponse', config: {...} }
      nr.action = nr.action || { type: 'modifyRequest', config: {} };
      return nr;
    });
  }

  function methodMatches(rule, method) {
    if (!rule._methods || rule._methods.length === 0) return true;
    return rule._methods.includes(method.toUpperCase());
  }

  function urlMatches(rule, url) {
    if (rule._patternType === 'regex') {
      return rule._regex ? rule._regex.test(url) : false;
    }
    // string match: includes
    return (rule._patternValue || '') && url.includes(rule._patternValue);
  }

  function findFirstMatch(url, method) {
    if (!enabled) return null;
    for (const r of rules) {
      if (!r.enabled) continue;
      if (!urlMatches(r, url)) continue;
      if (!methodMatches(r, method)) continue;
      return r;
    }
    return null;
  }

  function cloneHeadersToObject(headers) {
    const obj = {};
    try {
      for (const [k, v] of headers.entries()) {
        obj[k] = v;
      }
    } catch {}
    return obj;
  }

  function applyHeaderPatches(current, patch) {
    const next = new Headers(current || {});
    if (patch?.remove && Array.isArray(patch.remove)) {
      patch.remove.forEach((k) => next.delete(k));
    }
    if (patch?.set && typeof patch.set === 'object') {
      Object.entries(patch.set).forEach(([k, v]) => next.set(k, String(v)));
    }
    return next;
  }

  function isTextualContentType(ct) {
    if (!ct) return false;
    const l = ct.toLowerCase();
    return l.includes('application/json') || l.includes('text/') || l.includes('application/javascript');
  }

  function tryRegexReplace(input, ruleBody) {
    if (!ruleBody) return input;
    const mode = ruleBody.mode || 'regexReplace';
    if (mode === 'replace') {
      const find = String(ruleBody.find || '');
      const rep = String(ruleBody.replace || '');
      return input.split(find).join(rep);
    }
    // regexReplace
    let pattern = ruleBody.find;
    if (!pattern) return input;
    try {
      let regex;
      if (typeof pattern === 'string') {
        const m = pattern.match(/^\/(.*)\/([gimsuy]*)$/);
        if (m) regex = new RegExp(m[1], m[2]);
        else regex = new RegExp(pattern, 'g');
      } else if (pattern instanceof RegExp) {
        regex = pattern;
      }
      const rep = String(ruleBody.replace || '');
      return input.replace(regex, rep);
    } catch {
      return input;
    }
  }

  // Fetch hook
  const _origFetch = window.fetch.bind(window);

  window.fetch = async function apiProxyFetch(input, init = {}) {
    try {
      // Resolve URL and method
      let url = typeof input === 'string' ? input : (input && input.url) || '';
      let method = (init.method || (input && input.method) || 'GET').toUpperCase();

      const rule = findFirstMatch(url, method);
      try { if (rule) logInfo('Matched rule', { id: rule.id, name: rule.name, action: rule.action?.type, url, method }); } catch {}

      // Prepare request modifications
      let reqUrl = url;
      let reqInit = { ...init };
      let ruleId = rule?.id || null;

      if (rule && (rule.action?.type === 'modifyRequest' || rule.action?.type === 'modifyResponse' || rule.action?.type === 'mockResponse')) {
        const cfg = rule.action.config || {};

        if (rule.action.type === 'modifyRequest' || rule.action.type === 'modifyResponse') {
          // URL replace
          if (cfg.request?.urlReplace) {
            const { from, to, isRegex } = cfg.request.urlReplace;
            if (from) {
              if (isRegex) {
                try {
                  const m = String(from).match(/^\/(.*)\/([gimsuy]*)$/);
                  const re = m ? new RegExp(m[1], m[2]) : new RegExp(from, 'g');
                  reqUrl = reqUrl.replace(re, String(to ?? ''));
                } catch {
                  reqUrl = reqUrl.split(String(from)).join(String(to ?? ''));
                }
              } else {
                reqUrl = reqUrl.split(String(from)).join(String(to ?? ''));
              }
            }
          }

          // Headers patch
          if (cfg.request?.headers) {
            const base = new Headers((init && init.headers) || (typeof input !== 'string' && input && input.headers) || {});
            const patched = applyHeaderPatches(base, cfg.request.headers);
            reqInit.headers = patched;
          }

          // Body replace (only for string/JSON bodies we control)
          if (cfg.request?.body && reqInit && reqInit.body != null) {
            let bodyText = null;
            if (typeof reqInit.body === 'string') {
              bodyText = reqInit.body;
            } else if (reqInit.body && typeof reqInit.body === 'object' && !(reqInit.body instanceof FormData) && !(reqInit.body instanceof URLSearchParams) && !(reqInit.body instanceof Blob)) {
              try {
                bodyText = JSON.stringify(reqInit.body);
                // set content-type if missing
                const hs = new Headers(reqInit.headers || (typeof input !== 'string' && input && input.headers) || {});
                if (!hs.has('content-type')) hs.set('content-type', 'application/json;charset=UTF-8');
                reqInit.headers = hs;
              } catch {}
            }
            if (bodyText != null) {
              const replaced = tryRegexReplace(bodyText, cfg.request.body);
              reqInit.body = replaced;
            }
          }
        }

        // Mock response
        if (rule.action.type === 'mockResponse') {
          const mock = cfg.response || {};
          const status = Number(mock.status || 200);
          const headers = new Headers(mock.headers || {});
          let body = mock.body.replace || '';
          if (typeof body !== 'string') {
            try {
              body = JSON.stringify(body);
              if (!headers.has('content-type')) headers.set('content-type', 'application/json;charset=UTF-8');
            } catch {
              body = String(body);
            }
          }
          postToContent('log', { url, method, ruleId, event: 'mockResponse' });
          return new Response(body, { status, headers });
        }
      }

      // Proceed with (maybe) modified request
      const resp = await _origFetch(reqUrl === url ? input : reqUrl, reqInit);

      // Modify response if needed
      if (rule && (rule.action?.type === 'modifyResponse' || rule.action?.type === 'modifyRequest')) {
        const cfg = rule.action.config || {};
        const headersObj = cloneHeadersToObject(resp.headers);
        const ct = headersObj['content-type'] || headersObj['Content-Type'] || '';
        let body = null;

        if (isTextualContentType(ct)) {
          try {
            body = await resp.text();
          } catch {
            body = null;
          }
        }

        let modifiedBody = body;
        if (body != null) {
          // body regex replace
          if (cfg.response?.body) {
            modifiedBody = tryRegexReplace(body, cfg.response.body);
          }
        }

        // headers patch
        let finalHeaders = new Headers(resp.headers);
        if (cfg.response?.headers) {
          finalHeaders = applyHeaderPatches(finalHeaders, cfg.response.headers);
        }
        if (modifiedBody != null) {
          // If content-length exists, remove it; length changed
          finalHeaders.delete('content-length');
          postToContent('log', { url: reqUrl, method, ruleId, event: 'modifyResponse' });
          return new Response(modifiedBody, {
            status: resp.status,
            statusText: resp.statusText,
            headers: finalHeaders
          });
        }
      }

      if (ruleId) {
        postToContent('log', { url: reqUrl, method, ruleId, event: 'passThrough' });
      }
      return resp;
    } catch (e) {
      // In case of unexpected errors, fall back to original fetch
      try {
        return await _origFetch(input, init);
      } catch (err) {
        throw err;
      }
    }
  };

  // XHR hook (best-effort for response mutation)
  (function hookXHR() {
    const OrigXHR = window.XMLHttpRequest;
    if (!OrigXHR) return;

    const proto = OrigXHR.prototype;
    const origOpen = proto.open;
    const origSend = proto.send;
    const origSetRequestHeader = proto.setRequestHeader;

    proto.open = function(method, url, async = true, user, password) {
      this.__apMeta = { method: String(method || 'GET').toUpperCase(), url: String(url || '') };
      // allow URL modification based on rules now
      const rule = findFirstMatch(this.__apMeta.url, this.__apMeta.method);
      try { if (rule) logInfo('Matched rule', { id: rule.id, name: rule.name, action: rule.action?.type, url: this.__apMeta.url, method: this.__apMeta.method }); } catch {}
      if (rule && (rule.action?.type === 'modifyRequest' || rule.action?.type === 'modifyResponse')) {
        const cfg = rule.action.config || {};
        if (cfg.request?.urlReplace) {
          const { from, to, isRegex } = cfg.request.urlReplace;
          if (from) {
            try {
              if (isRegex) {
                const m = String(from).match(/^\/(.*)\/([gimsuy]*)$/);
                const re = m ? new RegExp(m[1], m[2]) : new RegExp(from, 'g');
                url = String(url).replace(re, String(to ?? ''));
              } else {
                url = String(url).split(String(from)).join(String(to ?? ''));
              }
            } catch {}
          }
        }
        this.__apMeta.url = url;
      }
      return origOpen.call(this, method, url, async, user, password);
    };

    proto.setRequestHeader = function(name, value) {
      this.__apHeaders = this.__apHeaders || new Headers();
      this.__apHeaders.set(name, value);
      return origSetRequestHeader.call(this, name, value);
    };

    proto.send = function(body) {
      try {
        const meta = this.__apMeta || { method: 'GET', url: '' };
        const rule = findFirstMatch(meta.url, meta.method);
        try { if (rule) logInfo('Matched rule', { id: rule.id, name: rule.name, action: rule.action?.type, url: meta.url, method: meta.method }); } catch {}
        if (rule) {
          const cfg = rule.action?.config || {};

          // request headers patch
          if (rule.action?.type === 'modifyRequest' || rule.action?.type === 'modifyResponse') {
            if (cfg.request?.headers) {
              const patched = applyHeaderPatches(this.__apHeaders || new Headers(), cfg.request.headers);
              // apply to XHR by calling setRequestHeader (must be before send)
              try {
                // Clear cannot be done; just set new values
                patched.forEach((v, k) => {
                  origSetRequestHeader.call(this, k, v);
                });
              } catch {}
            }
            // request body replace (string only)
            if (body != null && typeof body === 'string' && cfg.request?.body) {
              body = tryRegexReplace(body, cfg.request.body);
            }
          }

          // Mock response
          if (rule.action?.type === 'mockResponse') {
            const mock = cfg.response || {};
            const status = Number(mock.status || 200);
            let responseText = mock.body.replace || '';
            if (typeof responseText !== 'string') {
              try {
                responseText = JSON.stringify(responseText);
              } catch {
                responseText = String(responseText);
              }
            }
            // Async dispatch ready states to mimic XHR
            setTimeout(() => {
              try {
                Object.defineProperty(this, 'readyState', { value: 4, configurable: true });
                Object.defineProperty(this, 'status', { value: status, configurable: true });
                Object.defineProperty(this, 'responseText', { value: responseText, configurable: true });
                Object.defineProperty(this, 'response', { value: responseText, configurable: true });
                this.onreadystatechange && this.onreadystatechange();
                this.onload && this.onload();
                this.onloadend && this.onloadend();
              } catch {
                // Fallback: just call onloadend
                this.onloadend && this.onloadend();
              }
            }, 0);
            postToContent('log', { url: meta.url, method: meta.method, ruleId: rule.id, event: 'mockResponse' });
            return; // do not send real request
          }

          // Response modification (best-effort)
          if (rule.action?.type === 'modifyResponse' || rule.action?.type === 'modifyRequest') {
            const cfgResp = cfg.response || {};
            const self = this;
            this.addEventListener('readystatechange', function () {
              if (self.readyState === 4 && typeof self.responseText === 'string' && cfgResp.body) {
                try {
                  const original = self.responseText;
                  const replaced = tryRegexReplace(original, cfgResp.body);
                  if (replaced !== original) {
                    Object.defineProperty(self, 'responseText', { configurable: true, get: () => replaced });
                    Object.defineProperty(self, 'response', { configurable: true, get: () => replaced });
                    postToContent('log', { url: meta.url, method: meta.method, ruleId: rule.id, event: 'modifyResponse' });
                  }
                } catch {
                  // ignore
                }
              }
            });
          }
        }
      } catch {
        // ignore
      }
      return origSend.call(this, body);
    };
  })();

  // Receive state from extension
  window.addEventListener('message', (evt) => {
    const data = evt?.data;
    if (!data || data.__chan !== CHANNEL_FROM_EXT) return;
    if (data.cmd === 'state') {
      const st = data.payload || {};
      enabled = !!st.enabled;
      rules = normalizeRules(st.rules || []);
    }
  });

  // Request initial state
  postToContent('requestState', {});
})();